//
// Created by yangentao on 2021/2/3.
// Copyright (c) 2021 CocoaPods. All rights reserved.
//

import Foundation
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

public extension View {
    //不需要指定gridParams, 默认值, 也可以布局, GridLayout有默认的行和列参数
    var gridParams: GridParams {
        get {
            ensureAttr(key: "__gridParam__") {
                GridParams()
            }
        }
        set {
            setAttr("__gridParam__", newValue)
//            if newValue.height == WrapContent || newValue.width == WrapContent {
//                self.needLayoutOnPropChanged()
//            }
        }
    }

    @discardableResult
    func gridParams(_ block: (GridParams) -> Void) -> Self {
        let p = gridParams
        block(p)
//        if p.height == WrapContent || p.width == WrapContent {
//            self.needLayoutOnPropChanged()
//        }
        return self
    }
}

public class GridParams: Applyable {

    @GreatEQ(minValue: 1)
    public var columnSpan: Int = 1
    @GreatEQ(minValue: 1)
    public var rowSpan: Int = 1

    @GreatEQ(minValue: 0)
    public var width: CGFloat = 0
    @GreatEQ(minValue: 0)
    public var height: CGFloat = 0
    public var gravityX: GravityX = .none
    public var gravityY: GravityY = .none
    public var margins: Edge = Edge()

}

public extension GridParams {
    func width(_ w: CGFloat) -> Self {
        self.width = w
        return self
    }

    func height(_ h: CGFloat) -> Self {
        self.height = h
        return self
    }
}

//vertical layout only!
public class GridLayout: BaseLayout {
    public var paddings: Edge = Edge() {
        didSet {
            postLayout()
        }
    }


    @GreatEQ(minValue: 1)
    public var columns: Int = 3 {
        didSet {
            postLayout()
        }
    }

    @GreatEQ(minValue: 0)
    public var spaceHor: CGFloat = 0 {
        didSet {
            postLayout()
        }
    }
    @GreatEQ(minValue: 0)
    public var spaceVer: CGFloat = 0 {
        didSet {
            postLayout()
        }
    }

    private var defaultColumnInfo: GridCellInfo = GridCellInfo(value: 0, weight: 1)
    private var defaultRowInfo: GridCellInfo = GridCellInfo(value: 60, weight: 0)
    private var columnInfoMap: [Int: GridCellInfo] = [:]
    private var rowInfoMap: [Int: GridCellInfo] = [:]

    public func setColumnInfoDefault(value: CGFloat, weight: CGFloat) {
        defaultColumnInfo = GridCellInfo(value: value, weight: weight)
        postLayout()
    }

    public func setRowInfoDefault(value: CGFloat, weight: CGFloat) {
        defaultRowInfo = GridCellInfo(value: value, weight: weight)
        postLayout()
    }

    public func setColumnInfo(_ col: Int, value: CGFloat, weight: CGFloat) {
        columnInfoMap[col] = GridCellInfo(value: value, weight: weight)
        postLayout()
    }

    public func setRowInfo(_ row: Int, value: CGFloat, weight: CGFloat) {
        self.rowInfoMap[row] = GridCellInfo(value: value, weight: weight)
        postLayout()
    }

    @discardableResult
    public func spaces(_ hor: CGFloat, _ ver: CGFloat) -> Self {
        self.spaceHor = hor
        self.spaceVer = ver
        return self
    }

    public var fixedHeight: CGFloat {
        var fh: CGFloat = paddings.top + paddings.bottom
        let childViews = self.subviews
        if childViews.isEmpty {
            return fh
        }
        let cells: CellMatrix = calcCellsVertical(childViews)
        for r in 0..<cells.rows {
            var rowH: CGFloat = 0
            for c in 0..<cells.cols {
                let cell = cells[r, c]
                if let p = cell?.param {
                    let hh: CGFloat = max(p.height, (self.rowInfoMap[r] ?? self.defaultRowInfo).value)
                    rowH = max(rowH, (hh + p.margins.top + p.margins.bottom))
                }
            }
            fh += rowH
        }
        fh += spaceVer * (cells.rows - 1)
        return fh
    }

    override func layoutChildren(_ calcSize: CGSize?) -> CGSize {
        let childViews = self.subviews
        if childViews.isEmpty {
            return .zero
        }
        let cells: CellMatrix = calcCellsVertical(childViews)
        calcWidthsVertical(cells)
        calcHeightsVertical(cells)
        let _ = calcRectVertical(cells, calcSize == nil)
//        let maxY = calcRectVertical(cells, calcSize == nil)
//        let sz = Size(width: self.bounds.width, height: maxY - self.bounds.minY)
            return calcMinSize(cells)
    }

    private func calcMinSize(_ cells: CellMatrix) -> Size {

        var h: CGFloat = 0
        for r in 0..<cells.rows {
            h = max(h, maxRowHeightFixed(r, cells))
            h = max(h, self.rowInfoMap[r]?.value ?? defaultRowInfo.value)
        }
        var w: CGFloat = 0
        for c in 0..<cells.cols {
            var a = max(w, maxColWidthFixed(c, cells))
            a = max(a, self.columnInfoMap[c]?.value ?? defaultColumnInfo.value)
            w += a
        }
        return Size(width: w, height: h)
    }

    private func calcRectVertical(_ cells: CellMatrix, _ assignRect: Bool) -> CGFloat {
        var maxY: CGFloat = self.bounds.minY

        for row in 0..<cells.rows {
            var y: CGFloat = paddings.top
            for i in 0..<row {
                y += (cells[i, 0]?.height ?? 0) + spaceVer
            }
            for col in 0..<cells.cols {
                guard let cell = cells[row, col], let view = cell.view else {
                    continue
                }
                if col > 0 && cell.view === cells[row, col - 1]?.view {
                    continue
                }
                if row > 0 && cell.view === cells[row - 1, col]?.view {
                    continue
                }
                let param = view.gridParams
                var x: CGFloat = paddings.left // (cell.width + hSpace) * cell.view.gridParams!.spanColumns
                for i in 0..<col {
                    x += (cells[row, i]?.width ?? 0) + spaceHor
                }

                var ww: CGFloat = 0
                for c in col..<(col + param.columnSpan) {
                    ww += cells[row, c]?.width ?? 0
                    ww += spaceHor
                }
                ww -= spaceHor

                var hh: CGFloat = 0
                for r in row..<(row + param.rowSpan) {
                    hh += cells[r, col]?.height ?? 0
                    hh += spaceHor
                }
                hh -= spaceHor
//                let h = (cell.height + vSpace) * param.spanRows - vSpace
                let rect = Rect(x: x, y: y, width: ww, height: hh)
                if let view = cell.view {
                    let rect = placeView(view, rect, row, col)
                    if assignRect {
                        view.constraintStorage.updateFrame(rect)
                    }
//                    view.frame = rect
                }
                maxY = max(maxY, rect.maxY)
            }
        }
        return maxY
    }

    private func placeView(_ view: View, _ rect: Rect, _ row: Int, _ col: Int) -> Rect {
        let param = view.gridParams
        let x: CGFloat
        let y: CGFloat
        let w: CGFloat
        let h: CGFloat

        switch param.gravityX {
        case .none, .fill:
            w = rect.width
            x = rect.minX
            break
        case .left:
            if param.width > 0 {
                w = param.width
            } else {
                let a = self.columnInfoMap[col] ?? self.defaultColumnInfo
                w = a.value
            }
            x = rect.minX
        case .right:
            if param.width > 0 {
                w = param.width
            } else {
                let a = self.columnInfoMap[col] ?? self.defaultColumnInfo
                w = a.value
            }
            x = rect.maxX - w
            break
        case .center:
            if param.width > 0 {
                w = param.width
            } else {
                let a = self.columnInfoMap[col] ?? self.defaultColumnInfo
                w = a.value
            }
            x = rect.center.x - w / 2
            break
        }
        switch param.gravityY {
        case .none, .fill:
            h = rect.height
            y = rect.minY
        case .top:
            if param.height > 0 {
                h = param.height
            } else {
                let a = self.rowInfoMap[row] ?? self.defaultRowInfo
                h = a.value
            }
            y = rect.minY
        case .bottom:
            if param.height > 0 {
                h = param.height
            } else {
                let a = self.rowInfoMap[row] ?? self.defaultRowInfo
                h = a.value
            }
            y = rect.maxY - h
        case .center:
            if param.height > 0 {
                h = param.height
            } else {
                let a = self.rowInfoMap[row] ?? self.defaultRowInfo
                h = a.value
            }
            y = rect.center.y - h / 2
        }
        var r = Rect(x: x, y: y, width: w, height: h)
        r.origin.x += param.margins.left
        r.origin.y += param.margins.top
        r.size.width -= param.margins.left + param.margins.right
        r.size.height -= param.margins.top + param.margins.bottom
        return r
    }

    private func maxColWidthFixed(_ col: Int, _ cells: CellMatrix) -> CGFloat {
        var w: CGFloat = 0
        for r in 0..<cells.rows {
            if let cell = cells[r, col] {
                if let p = cell.param {
                    let a = max(p.width, cell.view?.intrinsicContentSize.width ?? 0)
                    w = max(w, a + p.margins.left + p.margins.right)
                }
            }
        }
        return w
    }

    private func maxRowHeightFixed(_ row: Int, _ cells: CellMatrix) -> CGFloat {
        var h: CGFloat = 0
        for c in 0..<cells.cols {
            if let cell = cells[row, c] {
                if let p = cell.param {
                    let a = max(p.height, cell.view?.intrinsicContentSize.height ?? 0)
                    h = max(h, a + p.margins.top + p.margins.bottom)
                }
            }
        }
        return h
    }

    private func calcHeightsVertical(_ cells: CellMatrix) {

        var rowInfos: [GridCellInfo] = .init(repeating: GridCellInfo(other: defaultRowInfo), count: cells.rows)
        for i in 0..<cells.rows {
            if let info = rowInfoMap[i] {
                rowInfos[i] = info
            } else {
                let maxH = maxRowHeightFixed(i, cells)
                if maxH > defaultRowInfo.value {
                    rowInfos[i] = GridCellInfo(value: maxH, weight: 0)
                } else {
                    rowInfos[i] = defaultRowInfo
                }
            }
        }
        //for (k, v) in self.rowInfoMap {
        //	rowInfos[k] = v
        //}

        let totalValue: CGFloat = self.bounds.height - self.spaceVer * (cells.rows - 1) - paddings.top - paddings.bottom
        var weightSum: CGFloat = 0
        var ls: [GridCellInfo] = []
        var ls2: [GridCellInfo] = []
        var leftValue: CGFloat = totalValue

        for r in 0..<cells.rows {
            let info = rowInfos[r]
            info.realValue = GRID_UNSPEC
            if info.weight > 0 {
                weightSum += info.weight
                if info.value > 0 {
                    ls += info
                } else {
                    ls2 += info
                }
            } else {
                //info.realValue = max(info.value, maxRowHeight(r, cells))
                info.realValue = info.value
                leftValue -= info.value
            }
        }
        for info in ls.sortedAsc({ $0.value }) {
            let v = leftValue * info.weight / weightSum
            if v < info.value {
                info.realValue = info.value
            } else {
                info.realValue = v
            }
            leftValue -= info.realValue
            weightSum -= info.weight
        }
        for info in ls2 {
            info.realValue = leftValue * info.weight / weightSum
        }
        for row in 0..<cells.rows {
            for col in 0..<cells.cols {
                cells[row, col]?.height = rowInfos[row].realValue.geZero
            }
        }
    }

    private func calcWidthsVertical(_ cells: CellMatrix) {
        var columnInfos: [GridCellInfo] = .init(repeating: GridCellInfo(other: defaultColumnInfo), count: cells.cols)
        for (k, v) in self.columnInfoMap {
            columnInfos[k] = v
        }

        let totalValue: CGFloat = self.bounds.width - self.spaceHor * (self.columns - 1) - paddings.left - paddings.right
        var leftValue: CGFloat = totalValue

        var weightSum: CGFloat = 0
        var ls: [GridCellInfo] = []
        var ls2: [GridCellInfo] = []

        for c in 0..<self.columns {
            let info = columnInfos[c]
            info.realValue = GRID_UNSPEC
            if info.weight > 0 {
                weightSum += info.weight
                if info.value > 0 {
                    ls += info
                } else {
                    ls2 += info
                }
            } else {
                info.realValue = info.value
                leftValue -= info.value
            }
        }
        // weight > 0 and value > 0
        for info in ls.sortedAsc({ $0.value }) {
            let v = leftValue * info.weight / weightSum
            if v < info.value {
                info.realValue = info.value
            } else {
                info.realValue = v
            }
            leftValue -= info.realValue
            weightSum -= info.weight
        }
        for info in ls2 {
            info.realValue = leftValue * info.weight / weightSum
        }

        for row in 0..<cells.rows {
            for col in 0..<cells.cols {
                cells[row, col]?.width = columnInfos[col].realValue
            }
        }
    }

    private func calcCellsVertical(_ viewList: [View]) -> CellMatrix {
        let cellMatrix = CellMatrix(cols: self.columns)
        var row = 0
        var col = 0
        for v in viewList {
            pos(v, matrix: cellMatrix, row: &row, col: &col)
        }
        return cellMatrix
    }

    private func pos(_ v: View, matrix: CellMatrix, row: inout Int, col: inout Int) {
        let param = v.gridParams
        while matrix[row, col] != nil {
            col += 1
            if col >= self.columns {
                row += 1
                col = 0
            }
        }
        let colSpan = min(self.columns, param.columnSpan)
        if col == 0 || col + colSpan - 1 < self.columns {
            for r in 0..<param.rowSpan {
                for c in 0..<colSpan {
                    matrix[row + r, col + c] = CellItem(v)
                }
            }
            return
        }
        matrix[row, col] = CellItem(nil)
        col += 1
        if col >= self.columns {
            row += 1
            col = 0
        }
        pos(v, matrix: matrix, row: &row, col: &col)

    }
}

fileprivate class CellItem {
    var view: View?
    lazy var param: GridParams? = view?.gridParams
    var left: CGFloat = 0
    var right: CGFloat = 0
    var width: CGFloat = 0
    var height: CGFloat = 0

    init(_ v: View?) {
        self.view = v
    }
}

fileprivate let GRID_UNSPEC: CGFloat = -1

public class GridCellInfo {
    @GreatEQ(minValue: 0)
    public var weight: CGFloat = 0
    @GreatEQ(minValue: 0)
    public var value: CGFloat = 0

    fileprivate var realValue: CGFloat = GRID_UNSPEC

    public init(value: CGFloat, weight: CGFloat) {
        self.value = value
        self.weight = weight
    }

    public convenience init(other: GridCellInfo) {
        self.init(value: other.value, weight: other.weight)
    }
}

fileprivate struct Coords: Hashable {
    let row: Int
    let col: Int
}

fileprivate class CellMatrix {
    var map: [Coords: CellItem] = [:]

    let cols: Int

    init(cols: Int) {
        assert(cols > 0)
        self.cols = cols
    }

    var rows: Int {
        if let a = map.keySet.max(by: { a, b in
            a.row < b.row
        }) {
            return a.row + 1
        }
        return 0
    }

    subscript(row: Int, col: Int) -> CellItem? {
        get {
            return map[Coords(row: row, col: col)]
        }
        set {
            map[Coords(row: row, col: col)] = newValue
        }
    }
}
